Skip to content

fix(compiler): convert absolute length units to pixels - #492

Open
YevheniiKotyrlo wants to merge 8 commits into
nativewind:mainfrom
YevheniiKotyrlo:fix/absolute-length-units
Open

YevheniiKotyrlo wants to merge 8 commits into
nativewind:mainfrom
YevheniiKotyrlo:fix/absolute-length-units

Conversation

@YevheniiKotyrlo

@YevheniiKotyrlo YevheniiKotyrlo commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Problem

in, cm, mm, Q, pt and pc are absolute lengths with fixed pixel values (css-values-4). parseLength warns and drops all six, so on native a declaration holding one sets nothing - width: 1in, border-top-width: 3pt, margin: 1in 1pc - and neither does a variable that resolves at runtime or a media query such as (min-width: 4in).

Solution

parseLength converts the six units to pixels through a table beside it and hands the result to its px branch, so an infinite or overflowing absolute length clamps as calc(infinity * 1px) does. Declarations, shorthands, transforms, shadows, font sizes, variables, runtime calc(), media and container queries and @property initial values all read their lengths through parseLength, so all of them convert, with or without inlineRem; lightningcss already folds a calc() mixing them with px.

Tests

src/__tests__/native/absolute-units.test.tsx renders each unit as a declaration and through a variable with inlineVariables: false, with inlineRem: false, in media queries either side of a 750px window and a container query either side of 4in, in a shorthand, a transform, a shadow, a font size, a runtime calc() and an @property initial value, in upper case and negative, and as calc(infinity * 1in) beside its px spelling. All but the container narrower than 4in fail on main. A finite case expects css-values-4's ratio, rounded as the compiler rounds every length, and an infinite one the value its px spelling compiles to; Chromium, Firefox and WebKit compute the same for every finite case but the sub-pixel 10mm and 4Q, which each engine snaps to its layout unit, and read each infinite one as its px spelling (readings).

Verification

On Ubuntu with the workflow's steps - build, lint, typecheck, yarn test, node --test node/__tests__/*.mjs, both library builds, the web export, the unstaged-changes checks and commitlint - everything passes merged with #500, which fixes the vendor Tailwind cases that flake on main; without it, those cases are the only failures.

The iOS dev build passes on macOS 27.0.1 with Xcode 27.0, where CI pins 26.6.

With this change :root { font-size: 12pt } sets the root em to 16, where main warned and dropped it, while compile-time rem still reads a :root font size only in px and stays at 14.

Unchanged, and older than this change: an absolute length set from JavaScript through vars() reaches React Native as the string it was given, as docs/css-variable-inputs.md has lengths set in pixels; an infinite rem, em, vw or vh length skips the px clamp; a runtime calc() drops infinity; the px branch, which the six units now share, keeps a NaN calc() and ignores the property's range; and a resolution media feature reads dpi at 160 a dppx.

A search of the issues and pull requests for absolute, pt, inch, unit and length units finds #458, which also changes parseLength and merges cleanly with this.

Device evidence, before and after: #492 (comment)

css-values-4 fixes in, cm, mm, Q, pt and pc against the inch (1in =
96px), so each has an exact pixel value. parseLength warned and dropped
all six, so a declaration, a variable and a media query holding one set
nothing on native.
@YevheniiKotyrlo

Copy link
Copy Markdown
Contributor Author

Device evidence — before / after

UNFIXED — Only the control is 96 pixels wide. Every other bar spans the row, because a width in an absolute unit other than px is dropped.

FIXED — Every bar is as wide as the control: an inch is 96 pixels, and centimetres, millimetres, quarter-millimetres, points and picas convert through it.

before — 3.1.0-rc.0 after — with this PR

Android 16 emulator (API 36, GLES on the host GPU), 1140×2400 @ 480dpi, both frames from one run. before is the published 3.1.0-rc.0 plus what my test app needs from my other patches to run: the native and native-internal subpath exports, PR 459, cascade-layer ranking, and a shim that compiles a class-qualified :root inert. after adds the rest of my open PRs, this one included. One of them shows on every screen: PR 417 narrows a var()-valued font-family to one family, so the after sets its text in the theme's face where the before falls back to the platform's.

@YevheniiKotyrlo
YevheniiKotyrlo marked this pull request as ready for review October 16, 2026 15:00
@YevheniiKotyrlo
YevheniiKotyrlo marked this pull request as draft October 19, 2026 17:27
@YevheniiKotyrlo

YevheniiKotyrlo commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor Author

Browser readings — the expected values

What Chromium 153, Firefox 155 and WebKit 26.6 compute for each case (getComputedStyle, headless), beside the value the test expects:

Case Test Chromium Firefox WebKit
width: 1in 96 96px 96px 96px
width: 2.54cm 96 96px 96px 96px
width: 10mm 37.7953 37.7812px 37.8px 37.78125px
width: 4Q 3.7795 3.76562px 3.78333px 3.765625px
width: 3pt 4 4px 4px 4px
width: 1pc 16 16px 16px 16px
width: 1IN, width: 2.54CM 96 96px 96px 96px
margin-left: -1pc -16 -16px -16px -16px
margin-top: -0.75pt -1 -1px -1px -1px
margin: 1in 1pc 96 16 96 16 96px 16px 96px 16px 96px 16px 96px 16px 96px 16px 96px 16px
transform: translateX(1in) translateX: 96 matrix(1, 0, 0, 1, 96, 0) matrix(1, 0, 0, 1, 96, 0) matrix(1, 0, 0, 1, 96, 0)
box-shadow: 3pt 6pt 1pc -3pt #000 4 8 16 -4 4px 8px 16px -4px 4px 8px 16px -4px 4px 8px 16px -4px
font-size: 12pt 16 16px 16px 16px
--width: 1in; width: var(--width) 96 96px 96px 96px
--width: 10px; width: calc(var(--width) + 1in) 106 106px 106px 106px
@property --width with initial-value: 1in, read by width: var(--width) 96 96px 96px 96px
(min-width: 7.8in) and (min-width: 7.9in) at a 750px viewport the first matches the first matches the first matches the first matches
@container (min-width: 4in) in a 390px and a 380px container the first matches the first matches the first matches the first matches
width: calc(infinity * 1in) beside calc(infinity * 1px) 9007199254740990, the px spelling's both 3.35544e+07px both 1.78957e+7px both 33554428px
border-radius: calc(infinity * 1in) beside calc(infinity * 1px) 9007199254740990, the px spelling's both 3.35544e+07px both 3.40282e38px both 340282346638528859811704183484516925440px
--r: calc(infinity * 1pt); border-radius: var(--r) beside its px spelling 9007199254740991, the px spelling's both 3.35544e+07px both 3.40282e38px both 340282346638528859811704183484516925440px

Each engine clamps an infinite length to its own largest one, so the infinite rows show the inch spelling reading as the px one; the test pins the value the compiler gives the px spelling. 10mm and 4Q are the two cases whose pixels are not whole: each engine reports the width its layout snaps to, 1/64 px in Chromium and WebKit and 1/60 px in Firefox, while the test keeps the css-values-4 ratio, rounded as the compiler rounds.

@YevheniiKotyrlo
YevheniiKotyrlo marked this pull request as ready for review October 19, 2026 21:27

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant